React TypeScript -- React 使用 TypeScript 开发

React TypeScript

1. 概述

1.1 创建应用

创建支持 TypeScript 语法的 React 应用

npx create-react-app <appname> --template typescript
1

1.2 文件后缀

如果文件中包含 React 组件或者 JSX 代码,文件后缀使用 tsx

如果文件中不包含任何 JSX 代码,文件后缀使用 ts

2. 为组件添加类型

在我们定义了组件以后,TypeScript 编译器并不知道我们定义的是组件,它会认为我们定义的就是一个普通的函数。

在类型认知出现偏差以后,TypeScript 编译器不能正确的对我们的代码进行约束。

比如在下列代码中,我们通过组件获取组件下的属性,TypeScript 编译器会报错,说组件下不存在这个属性。

当 TypeScript 编译器知道我们定义的是组件以后,当我们错误的使用了组件以后,它才能准确的为我们进行提示。

const Child= () => {
  return <div>Child</div>;
};

// 类型 "() => Element" 上不存在属性 "displayName"。
console.log(Child.displayName);
// 类型 "() => Element" 上不存在属性 "defaultProps"。
console.log(Child.defaultProps);
1
2
3
4
5
6
7
8
import { FC } from "react";

const Child: FC = () => {
  return <div>Child</div>;
};
1
2
3
4
5

3. Props

为组件 props 定义接口类型,编译器可以检查父组件在调用该组件时是否正确的传递了 props,在子组件内部是否正确的使用了 props。

// src/props/Child.tsx
interface Props {
  color: string;
  onClick: () => void;
}

const Child: FC<Props> = ({ color, onClick }) => {
  return <div onClick={onClick}>{color}</div>;
};
1
2
3
4
5
6
7
8
9
// src/props/Parent.tsx
const Parent = () => {
  return <Child color="red" onClick={() => console.log("clicked")} />;
};
1
2
3
4

4. state

// src/state/Guests.tsx
import { useState, FC } from "react";

const Guests: FC = () => {
  const [name, setName] = useState<string>("");
  // 此处如果不为 guests 指定类型, 类型将会是 never[]
  const [guests, setGuests] = useState<string[]>([]);
  const clickHandler = () => {
    setName("");
    // 如果 guests 是 never[], 那么字符串 name 将不能被存储到 guests 数组中
    setGuests([...guests, name]);
  };
  return (
    <>
      <ul> {guests.map((guest) => <li key={guest}>{guest}</li>)}</ul>
      <input type="text" value={name} onChange={(event) => setName(event.target.value)}/>
      <button onClick={clickHandler}>add</button>
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/state/UserSearch.tsx
import { useState, FC } from "react";

const users = [
  { name: "张三", age: 20 },
  { name: "李四", age: 30 },
];

const UserSearch: FC = () => {
  const [name, setName] = useState<string>("");
  // 在组件初次渲染, 在没有找到 user 的情况下, user 的类型是 undefined
  // 在找到 user 以后, 它的类型是 {name: string, age: number}
  // 所以 user 的类型就应该是 {name: string, age: number} | undefined
  const [user, setUser] = useState<{ name: string; age: number } | undefined>();
	// 搜索用户
  const searchHandler = () => {
    // find 方法的返回值可能是 user, 也可能是 undefined
    setUser(
    	users.find((user) => user.name === name)
    );
  };
  return (
    <>
      <input type="text" value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={searchHandler}>search</button>
      {user && JSON.stringify(user)}
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5. 事件对象

// src/event/EventComponent.tsx
import { ChangeEvent, FC, DragEvent } from "react";

const EventComponent: FC = () => {
  // 参数"event"隐式具有"any"类型
  const changeHandler = (event: ChangeEvent<HTMLInputElement>) => {
    console.log(event.target.value);
  };
  const dragStartHandler = (event: DragEvent<HTMLDivElement>) => {
    // event.target: 返回触发事件的元素
    // event.currentTarget: 返回绑定事件的元素
    console.log(event.target);
    console.log(event.currentTarget);
  };
  return (
    <>
      <input type="text" onChange={changeHandler} />
      <div draggable onDragStart={dragStartHandler}> drag event </div>
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

43

6. ref

src/ref/RefComponent.tsx

import { FC, useRef, useEffect } from "React";

const RefComponent: FC = () => {
  const inputRef = useRef<HTMLInputElement | null>(null);
  useEffect(() => {
    if (!inputRef.current) return;
    inputRef.current.focus();
  }, []);
  return <input ref={inputRef} />;
};
1
2
3
4
5
6
7
8
9
10

7. Redux

npm install redux redux-thunk axios react-redux @types/react-redux --save-exact
# save-exact: 在 package.json 文件中记录安装包的确切版本
1
2

redux、redux-thunk、axios 内置 TypeScript 类型声明文件,所以不需要单独下载。

react-redux 没有内置类型声明文件,所以需要单独下载。

44

45

需求:向 npm 发送请求加载 npm 包列表信息。

第一步:定义 Action Type

// src/state/action-types/package.action.types.ts
export enum searchActionType {
  // 请求中
  SEARCH_PACKAGES = "search_packages",
  // 请求成功
  SEARCH_PACKAGES_SUCCESS = "search_packages_success",
  // 请求失败
  SEARCH_PACKAGES_ERROR = "search_packages_error",
}
1
2
3
4
5
6
7
8
9
// src/state/action-types/index.ts
export * from "./package.action.types";
1
2

第二步:定义 Action 对象类型、Reducer 函数的 action 参数 action 类型

// src/state/actions/packages.action.ts
import { searchActionType } from "../action-types";

/**
 * 请求: {type: "search_packages"}
 * 成功: {type: "search_packages_success", payload: ["react", "react-dom"]}
 * 失败: {type: "search_packages_error", error: "Request Failed"}
 */

interface SearchPackagesAction {
  type: searchActionType.SEARCH_PACKAGES;
}

interface SearchPackagesSuccessAction {
  type: searchActionType.SEARCH_PACKAGES_SUCCESS;
  payload: string[];
}

interface SearchPackagesErrorAction {
  type: searchActionType.SEARCH_PACKAGES_ERROR;
  error: string;
}

export type SearchAction =
  | SearchPackagesAction
  | SearchPackagesSuccessAction
  | SearchPackagesErrorAction;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/state/actions/index.ts
export * from "./packages.action";
1
2

第三步:创建 Reducer 函数,匹配 Action Type 返回对应的状态

// src/state/reducers/packages.reducer.ts
import { searchActionType } from "../action-types";
import { SearchAction } from "../actions";

export interface PackagesState {
  loading: boolean;
  error: string | null;
  list: string[];
}

const initialState: PackagesState = {
  loading: false,
  error: null,
  list: [],
};

export default function packagesReducer(
  state: PackagesState = initialState,
  action: SearchAction
): PackagesState {
  switch (action.type) {
    case searchActionType.SEARCH_PACKAGES:
      return { loading: true, error: null, list: [] };
    case searchActionType.SEARCH_PACKAGES_SUCCESS:
      return { loading: false, error: null, list: action.payload };
    case searchActionType.SEARCH_PACKAGES_ERROR:
      return { loading: false, error: action.error, list: [] };
    default:
      return state;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

第四步:合并 reducer 函数

// src/state/reducers/index.ts
import { combineReducers } from "redux";
import packagesReducer from "./packages.reducer";

export const reducers = combineReducers({
  packages: packagesReducer,
});
1
2
3
4
5
6
7

第五步:创建用于发送请求获取 npm 包的 action creator 函数

// src/state/action-creators/packages.action.creators.ts
import axios from "axios";
import { Dispatch } from "react";
import { searchActionType } from "../action-types";
import { SearchAction } from "../actions";

export const searchPackages =
  (key: string) => async (dispatch: Dispatch<SearchAction>) => {
    dispatch({
      type: searchActionType.SEARCH_PACKAGES,
    });
    try {
      const { data } = await axios.get(
        `https://registry.npmjs.org/-/v1/search`,
        {
          params: {
            text: key,
          },
        }
      );
      dispatch({
        type: searchActionType.SEARCH_PACKAGES_SUCCESS,
        payload: data.objects.map((item: any) => item.package.name),
      });
    } catch (error) {
      if (error instanceof Error) {
        dispatch({
          type: searchActionType.SEARCH_PACKAGES_ERROR,
          error: error.message,
        });
      }
    }
  };

// unknow 是更加严格的 any 类型.
// 在对 unknown 类型的值执行大多数操作之前, 我们必须进行某种形式的检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export * from "./packages.action.creators";
1

第五步:创建 Store 对象,配置 redux-thunk 中间件函数

import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import { reducers } from ".";

export const store = createStore(reducers, applyMiddleware(thunk));
1
2
3
4
5

第六步:创建 state 入口文件

// src/state/index.ts
export * as actionCreators from "./action-creators";
export * from "./reducers";
export * from "./store";
1
2
3
4

第七步:配置 Provider 组件

// src/components/App.tsx
import { FC } from "react";
import { Provider } from "react-redux";
import { store } from "../state";
import Packages from "./Packages";

export const App: FC = () => (
  <Provider store={store}>
    <Packages />
  </Provider>
);
1
2
3
4
5
6
7
8
9
10
11

第八步:在组件中,当点击按钮时向服务器端发送请求获取 npm 包

// src/components/Packages.tsx
import { FC, FormEvent, useState } from "react";
import { useActions } from "../hooks/useActions";

const Packages: FC = () => {
  const [key, setKey] = useState<string>("");
  const { searchPackages } = useActions();

  const onSubmitHandler = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    searchPackages(key);
  };
  return (
    <form onSubmit={onSubmitHandler}>
      <input type="text" value={key} onChange={(event) => setKey(event.target.value)} />
      <button>search</button>
    </form>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/hooks/useActions.ts
import { useDispatch } from "react-redux";
import { bindActionCreators } from "redux";
import { actionCreators } from "../state";

export const useActions = () => {
  const dispatch = useDispatch();
  return bindActionCreators(actionCreators, dispatch);
};
1
2
3
4
5
6
7
8
9

第九步:在组件中获取状态并根据状态渲染 UI

import { useSelector } from "react-redux";
import { AppState } from "../state";
import { PackagesState } from "../state/reducers/packages.reducer";

const Packages: FC = () => {
  const state = useSelector<AppState, PackagesState>((state) => state.packages);

  return (
    <>
      {state.loading && <div>loading....</div>}
      {state.error && <div>{state.error}</div>}
      {state.list.map((name) => (
        <div key={name}>{name}</div>
      ))}
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

定义应用全局状态的类型,用于传递给 useSelecter 钩子函数

// src/state/reducers/index.ts
export type AppState = ReturnType<typeof reducers>;
1
2

第十步:优化为应用全局状态设置类型的代码

// src/hooks/useTypedSelector.ts
import { useSelector, TypedUseSelectorHook } from "react-redux";
import { AppState } from "../state/reducers";

export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
1
2
3
4
5
// src/components/Packages.tsx
import { useTypedSelector } from "../hooks/useTypedSelector";

const Packages: FC = () => {
  const state = useTypedSelector((state) => state.packages);
};
1
2
3
4
5
6